package cn.claim.storage.node;

import cn.claim.storage.tools.*;
import org.apache.commons.io.FileUtils;

import java.io.*;
import java.net.*;
import java.util.Objects;
import java.util.Scanner;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * Created by claim on 17-7-8.
 */
public final class StorageNode {
    private static long spareSpace;
    private static long tempSpace;
    private static long totalVolume;
    private static long fileNum;
    private final static Object spareSpaceLock = new Object();

    private static void init(String path) {
        String dir = Config.getProp("saveDirectory");
        if (dir == null) {
            Config.setProp("saveDirectory", path);
        }
        File saveDir = new File(Config.getProp("saveDirectory"));
        if (saveDir.exists()) {
            if (saveDir.isFile()) {
                throw new IllegalArgumentException("指定的目录错误，请检查配置文件Config.properties");
            }
        } else
            //noinspection ResultOfMethodCallIgnored
            saveDir.mkdirs();
    }

    private static void resize() {
        String spare = Config.getProp("volume", "100MB");
        File saveDir = new File(Config.getProp("saveDirectory"));
        synchronized (spareSpaceLock) {
            switch (spare.substring(spare.length() - 2).toLowerCase()) {
                case "kb":
                    spareSpace = Long.parseLong(spare.substring(0, spare.length() - 2)) * 1024;
                    break;
                case "mb":
                    spareSpace = Long.parseLong(spare.substring(0, spare.length() - 2)) * 1024 * 1024;
                    break;
                case "gb":
                    spareSpace = Long.parseLong(spare.substring(0, spare.length() - 2)) * 1024 * 1024 * 1024;
                    break;
                default:
                    System.out.println("空闲空间配置有误，请检查配置文件config.properties");
                    return;
            }
            totalVolume = spareSpace;
            spareSpace -= FileUtils.sizeOfDirectory(saveDir);
            String[] children = saveDir.list();
            fileNum = children == null ? 0 : children.length;
        }
    }

    /**
     * <em>Invoke the method as early as possible. If the port is not available, the method may cause a RuntimeException in your VM.</em>
     *
     * @throws IllegalArgumentException when port is not available
     */
    private static ServerSocket getServerSocketWithoutExceptions(int port) {
        try {
            return new ServerSocket(port);
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(1);
        }
        return null;
    }

    private static boolean backup(NodeData backupNode, File file) {
        try {
            Socket socket;
            FileUploader uploader;
            PrintWriter request;
            Scanner response;
            String queryData = new ClientQueryData(DataTools.calculateMd5(file), file.getName()).toJSON();
            int retry = 5;
            while (true) {
                socket = new Socket(backupNode.getIp(), backupNode.getUploadPort());
                uploader = new FileUploader(file, socket);
                request = new PrintWriter(socket.getOutputStream());
                response = new Scanner(socket.getInputStream());

                request.println(queryData);
                request.println(file.length());
                request.flush();
                long range = Long.parseLong(response.nextLine());

                if (uploader.upload(range) > 0) {
                    socket.close();
                    Thread.sleep(5000);
                    socket = new Socket(backupNode.getIp(), backupNode.getQueryPort());
                    request = new PrintWriter(socket.getOutputStream());
                    response = new Scanner(socket.getInputStream());

                    request.println("check");
                    request.println(queryData);
                    request.flush();
                    long length = Long.parseLong(response.nextLine());

                    if (length == file.length()) {
                        socket.close();
                        break;
                    } else if (--retry <= 0) {
                        return false;
                    }
                }
                socket.close();
            }
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
        return true;
    }

    private static class QueryListenTask implements Runnable {
        private final Socket socket;
        private String dataDirectory;


        /**
         * Initialize a queryListener task on the supplied dir / Socket.
         */
        public QueryListenTask(Socket socket, String dataDirectory) {
            this.socket = socket;
            this.dataDirectory = dataDirectory;
            if (dataDirectory.charAt(dataDirectory.length() - 1) != '/') {
                this.dataDirectory += "/";
            }
        }

        @Override
        public void run() {
            try {
                Scanner request = new Scanner(socket.getInputStream());
                PrintStream response = new PrintStream(socket.getOutputStream());

                String order = request.nextLine();
                switch (order) {
                    case "check": {
                        ClientQueryData data = JSONParsable.parse(request.nextLine(), ClientQueryData.class);
                        String md5 = data.getMd5(), uuid = data.getUuid();
                        File file = new File(dataDirectory + uuid);

                        if (!file.exists() || !Objects.equals(DataTools.calculateMd5(file), md5)) {
                            response.println(0);
                            response.flush();
                        }
                        response.println(file.length());
                        response.flush();
                        new JSONTransportingLogger(file).removeDataFile();
                        synchronized (spareSpaceLock) {
                            tempSpace += file.length();
                        }
                        break;
                    }
                    case "delete": {
                        String uuid = request.nextLine();
                        File file = new File(Config.getProp("saveDirectory") + uuid);
                        if (file.exists())
                            //noinspection ResultOfMethodCallIgnored
                            file.delete();
                        break;
                    }
                    case "backup": {
                        String uuid = request.nextLine();
                        String backupServerInfo = request.nextLine();
                        if (!Objects.equals(backupServerInfo, "null")) {
                            NodeData backupNode = JSONParsable.parse(backupServerInfo, NodeData.class);
                            File file = new File(Config.getProp("saveDirectory") + uuid);
                            int retry = 3;
                            while (!backup(backupNode, file) && retry > 0) {
                                retry--;
                                Thread.sleep(2000);
                            }
                            if (retry > 0)
                                response.println("finished");
                            else
                                response.println("failed");
                            response.flush();
                        }
                        break;
                    }
                }
            } catch (IOException | InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Listener Thread Exit");
        }
    }

    private static class FileAcceptTask implements Runnable {
        private final Socket source;

        public FileAcceptTask(Socket source) {
            this.source = source;
        }

        @Override
        public void run() {
            try {
                //Handle request
                Scanner request = new Scanner(source.getInputStream());
                ClientQueryData data = JSONParsable.parse(request.nextLine(), ClientQueryData.class);
                long totalLength = Long.parseLong(request.nextLine());
                PrintWriter response = new PrintWriter(source.getOutputStream());

                File accepting = new File(Config.getProp("saveDirectory") + data.getUuid());
                if (!accepting.exists()) //first time to accept this file
                    synchronized (spareSpaceLock) {
                        if (spareSpace + tempSpace > totalLength) {
                            tempSpace -= totalLength;
                            spareSpace -= totalLength;
                            fileNum++;
                        } else {
                            response.println(-1);
                            response.flush();
                            return;
                        }
                    }
                FileAccepter accepter = new FileAccepter(accepting, source);

                //Handle log
                TransportingData log = accepter.logger.getData();
                if (log == null) {
                    accepter.logger.log(data.getMd5(), 0);
                    log = accepter.logger.getData();
                }

                //Send response
                response.println(log.getRange());
                response.flush();

                accepter.accept(log.getRange());
            } catch (IOException e) {
                e.printStackTrace();
            }
            System.out.println("Acceptor thread log out");
        }
    }

    private static class FileDownloadTask implements Runnable {
        private final Socket socket;

        public FileDownloadTask(Socket socket) {
            this.socket = socket;
        }

        @Override
        public void run() {
            try {
                Scanner request = new Scanner(socket.getInputStream());
                PrintWriter response = new PrintWriter(socket.getOutputStream());
                String downloadId = request.nextLine();

                File transported = new File(Config.getProp("saveDirectory", "./data/") + downloadId);
                if (!transported.exists()) {
                    response.println("null");
                    response.flush();
                    return;
                } else {
                    response.println("transporting");
                    response.flush();
                }

                long range = Long.parseLong(request.nextLine());
                FileUploader uploader = new FileUploader(transported, socket);
                uploader.upload(range);
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        //Specify the directory of saved Data. The default directory is ./data .
        String path = args.length > 0 ? args[0] : "./data";
        init(path);
        resize();

        NodeData nodeData = NodeDataBuilder.newInstance()
                .withDownloadPort(Integer.parseInt(Config.getProp("downloadPort", "7000")))
                .withUploadPort(Integer.parseInt(Config.getProp("uploadPort", "6000")))
                .withIp("localhost")
                .withName(Config.getProp("nodeName", "node"))
                .withQueryPort(Integer.parseInt(Config.getProp("queryPort", "8000")))
                .withToTalVolume(totalVolume)
                .withSpareSpace(spareSpace)
                .build();


        final ServerSocket queryListener = getServerSocketWithoutExceptions(nodeData.getQueryPort()),
                fileAccepter = getServerSocketWithoutExceptions(nodeData.getUploadPort()),
                fileDownloader = getServerSocketWithoutExceptions(nodeData.getDownloadPort());


        //start QueryListenTask
        //noinspection TrivialFunctionalExpressionUsage
        (new Thread(() -> {
            ExecutorService listeners = Executors.newFixedThreadPool(4);
            try {
                //noinspection InfiniteLoopStatement
                while (true) {
                    @SuppressWarnings("ConstantConditions") Socket listener = queryListener.accept();
                    listeners.submit(new QueryListenTask(listener, path));
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        })).start();

        //start FileAccepter
        //noinspection TrivialFunctionalExpressionUsage
        new Thread(() -> {
            ExecutorService FileAcceptors = Executors.newFixedThreadPool(4);
            try {
                //noinspection InfiniteLoopStatement
                while (true) {
                    @SuppressWarnings("ConstantConditions") Socket accepter = fileAccepter.accept();
                    FileAcceptors.submit(new FileAcceptTask(accepter));
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }).start();

        //start FileDownloader
        //noinspection TrivialFunctionalExpressionUsage
        new Thread(() -> {
            ExecutorService FileDownloaders = Executors.newFixedThreadPool(4);
            try {
                //noinspection InfiniteLoopStatement
                while (true) {
                    @SuppressWarnings("ConstantConditions") Socket downloader = fileDownloader.accept();
                    FileDownloaders.submit(new FileDownloadTask(downloader));
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }).start();

        //start send packets to file server
        new Thread(() -> {
            try {
                DatagramSocket client = new DatagramSocket();
                Timer timer = new Timer();
                timer.schedule(new TimerTask() {
                    @Override
                    public void run() {
                        synchronized (nodeData) {
                            synchronized (spareSpaceLock) {
                                resize();
                                nodeData.setSpareSpace(spareSpace + tempSpace);
                                nodeData.setFileNum(fileNum);
                                String data = nodeData.toJSON();
                                try {
                                    DatagramPacket packet = new DatagramPacket(data.getBytes(),
                                            data.length(),
                                            InetAddress.getByName(Config.getProp("ip", "localhost")),
                                            Integer.parseInt(Config.getProp("udpPort", "4000")));
                                    client.send(packet);
                                } catch (IOException e) {
                                    e.printStackTrace();
                                }
                            }
                        }
                    }
                }, 0, 3000);
            } catch (SocketException e) {
                e.printStackTrace();
            }
        }).start();
    }
}
